// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.missile;

import bats.Assets;
import bats.SpatialProperties;
import bats.control.BatsDelayControl;
import bats.control.DeleteControl;
import bats.control.RevealControl;
import bats.control.physics.BatsBubbleControl;
import bats.scene.Physics;
import bats.scene.SceneNode;
import bats.unit.LaunchControl;
import bats.unit.OtoControl;
import com.jme3.audio.AudioNode;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.NameGenerator;
import jme3utilities.controls.AudioControl;

/**
 * The shafts in a particular scene. Shafts are slender missile weapons with
 * sharp tips (such as darts, crossbow bolts, arrows, assegais, harpoons,
 * pilums, and javelins).
 *
 * By convention, shafts point in the local +Z direction. In Blender, a shaft
 * should resemble a cylinder 1 BU long on the Y axis with radius 1 BU.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Shafts {
    // *************************************************************************
    // constants

    /**
     * average density of a shaft (in kilograms per liter)
     */
    final private static float density = 1.1f;
    /**
     * hardness setting (in meters/second)
     */
    final private static float hardness = 100f;
    /**
     * remaining life following a rebound (in seconds)
     */
    final private static float lifetimeAfterRebound = 2f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Shafts.class.getName());
    /**
     * base string for naming shaft spatials
     */
    final private static String namePrefix = "shaft";
    /**
     * asset path for rebound sound effect
     */
    final public static String tinkPath = "Sounds/effects/tink.wav";
    // *************************************************************************
    // fields
    /**
     * scene where these shafts reside: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate for the specified scene.
     *
     * @param scene where these shafts reside (not null)
     */
    public Shafts(SceneNode scene) {
        assert scene != null;
        this.scene = scene;
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Generate a collision shape for a shaft.
     *
     * @param spatial which shaft (not null)
     * @return a new collision shape
     */
    public CollisionShape createCollisionShape(Spatial spatial) {
        assert spatial != null;

        float length = getLength(spatial); // meters
        float radius = getRadius(spatial); // meters
        CollisionShape shape = createCollisionShape(length, radius);

        return shape;
    }

    /**
     * Compute the length of a shaft. Assumes that the model is one meter long
     * and aligned with the Z-axis.
     *
     * @param spatial which shaft (not null)
     * @return length (in meters, &gt;0)
     */
    public float getLength(Spatial spatial) {
        float length = spatial.getWorldScale().z * scene.getScaleFactor();

        assert length > 0f : length;
        return length;
    }

    /**
     * Compute the radius of a shaft. Assumes that the model is two model units
     * in diameter and aligned with the Z-axis.
     *
     * @param spatial which shaft (not null)
     * @return radius (in meters, &gt;0)
     */
    public float getRadius(Spatial spatial) {
        Vector3f worldScale = spatial.getWorldScale();

        float radius = worldScale.x;
        assert radius == worldScale.y : radius;
        radius *= scene.getScaleFactor();

        assert radius > 0f : radius;
        return radius;
    }

    /**
     * Test whether the specified spatial represents a shaft.
     *
     * @param spatial which spatial (not null)
     * @return true if the spatial represents a shaft, false otherwise
     */
    public static boolean isShaft(Spatial spatial) {
        String name = spatial.getName();
        boolean result = NameGenerator.isFrom(name, namePrefix);
        return result;
    }

    /**
     * Launch a shaft with a specified speed at its current elevation angle.
     *
     * @param launcher the unit which will launch the shaft (not null)
     * @param shaft which shaft (not null)
     * @param launchSpeed initial speed of the shaft (meters/sec)
     */
    public void launch(Spatial launcher, Spatial shaft, float launchSpeed) {
        assert launcher != null;
        assert shaft != null;
        release(launcher, shaft);

        Transform transform = shaft.getWorldTransform();
        Quaternion orientation = transform.getRotation();
        /*
         * Launch velocity should be in the shaft's +Z direction.
         */
        Vector3f worldDirection = orientation.mult(Vector3f.UNIT_Z);
        float speed = launchSpeed / scene.getScaleFactor(); // wu per sec
        Vector3f velocity = worldDirection.mult(speed);
        ShaftControl shaftControl = shaft.getControl(ShaftControl.class);
        shaftControl.setLinearVelocity(velocity);
    }

    /**
     * Load a new shaft onto an existing launcher.
     *
     * @param launcher (not null)
     * @param length shaft length (in meters, &gt;0)
     * @param radius shaft radius (in meters, &gt;0)
     * @param elevationAngle angle above the horizon (in radians)
     * @param localOffset (not null)
     * @return a new spatial
     */
    public Spatial load(Node launcher, float length, float radius,
            float elevationAngle, Vector3f localOffset) {
        assert radius > 0 : radius;
        assert length > radius : length;
        /*
         * Generate a unique name for this shaft.
         */
        NameGenerator nameGenerator = scene.getNameGenerator();
        String name = nameGenerator.unique(namePrefix);
        /*
         * Create the main node and mesh geometry for the shaft.
         */
        Node node = new Node(name);
        String path = "Models/missiles/shafts/pilum.j3o";
        int numGeometries = 1;
        Node model = Assets.loadModel(path, numGeometries);
        /*
         * Initialize the main node.
         */
        launcher.attachChild(node);
        float scaleFactor = scene.getScaleFactor();
        float launcherScale = launcher.getWorldScale().x;
        float rScale = radius / scaleFactor / launcherScale;
        float zScale = length / scaleFactor / launcherScale;
        node.scale(rScale, rScale, zScale);
        SpatialProperties.setHardness(model, hardness);
        SpatialProperties.setLauncher(node, launcher);
        /*
         * Give the geometry a glossy black material so it matches the otos.
         */
        Material lit = Assets.loadPilumMaterial();
        model.setMaterial(lit);
        /*
         * Shafts cast shadows but need not receive them.
         */
        model.setShadowMode(RenderQueue.ShadowMode.Cast);
        /*
         * Position the shaft at the launcher's launch point.
         */
        node.setLocalTranslation(localOffset);
        setElevation(node, elevationAngle);
        /*
         * Add controls for various behaviors.
         */
        float mass = calculateMass(node);
        CollisionShape shape = createCollisionShape(node);
        ShaftControl shaftControl = new ShaftControl(shape, mass, scene);
        Physics physics = scene.getWorld().getPhysics();
        physics.addObject(shaftControl);
        node.addControl(shaftControl);

        PenetrateControl penetrateControl = new PenetrateControl(shape, scene);
        physics.addObject(penetrateControl);
        node.addControl(penetrateControl);

        DeleteControl deleteControl = new DeleteControl(scene);
        node.addControl(deleteControl);

        BatsDelayControl delayControl = new BatsDelayControl();
        delayControl.addSlave(deleteControl);
        node.addControl(delayControl);

        PhysicsSpace space = physics.getSpace();
        BatsBubbleControl bubbleControl =
                new BatsBubbleControl(shape, space, deleteControl);
        node.addControl(bubbleControl);

        RevealControl revealControl = new RevealControl(model);
        node.addControl(revealControl);
        /*
         * Load sound effects.
         */
        AudioNode tinkNode = Assets.loadBufferedAudio(tinkPath);
        AudioControl audioControl = new AudioControl();
        tinkNode.addControl(audioControl);
        node.attachChild(tinkNode);

        return node;
    }

    /**
     * Schedule a shaft for deletion after a rebound.
     *
     * @param spatial which shaft (not null)
     */
    public void rebound(Spatial spatial) {
        logger.log(Level.INFO, "{0} will be deleted", spatial.getName());
        BatsDelayControl delayControl =
                spatial.getControl(BatsDelayControl.class);
        if (delayControl.getRemainingSeconds() > lifetimeAfterRebound) {
            delayControl.setRemainingSeconds(lifetimeAfterRebound);
        }
        delayControl.setEnabled(true);
    }

    /**
     * Release a shaft without launching it.
     *
     * @param launcher the unit which will release the shaft (not null)
     * @param shaft which shaft to release (not null)
     */
    public void release(Spatial launcher, Spatial shaft) {
        assert launcher != null;
        assert shaft != null;
        logger.log(Level.INFO, "{0} releases {1}", new Object[]{
            launcher.getName(), shaft.getName()
        });
        /*
         * Cleanly abort the launch.
         */
        LaunchControl launchControl = launcher.getControl(LaunchControl.class);
        launchControl.abort();
        /*
         * Re-parent the geometry from the launcher to the world.
         */
        Transform transform = shaft.getWorldTransform();
        scene.getWorld().attach(shaft);
        shaft.setLocalTransform(transform);
        OtoControl otoControl = launcher.getControl(OtoControl.class);
        otoControl.updateCollisionShape();
        /*
         * Enable the physics control (for gravity and collisions) and
         * position the object to match the launcher.
         */
        Vector3f location = transform.getTranslation();
        Quaternion orientation = transform.getRotation();
        Vector3f velocity = new Vector3f();
        ShaftControl shaftControl = shaft.getControl(ShaftControl.class);
        shaftControl.enable(location, orientation, velocity);
    }

    /**
     * Alter the elevation angle of a shaft.
     *
     * @param spatial which shaft (not null)
     * @param newAngle above the horizontal (in radians)
     */
    public void setElevation(Spatial spatial, float newAngle) {
        Quaternion elevate = new Quaternion();
        elevate.fromAngleNormalAxis(-newAngle, Vector3f.UNIT_X);
        spatial.setLocalRotation(elevate);
    }
    // *************************************************************************
    // private methods

    /**
     * Compute the mass of a shaft.
     *
     * @param spatial which shaft (not null)
     * @return mass (in kilograms, &gt;0)
     */
    private float calculateMass(Spatial spatial) {
        float radius = getRadius(spatial); // meters
        float length = getLength(spatial); // meters
        float litres = 1e3f * FastMath.PI * radius * radius * length;
        float kilograms = density * litres;

        assert kilograms > 0f : kilograms;
        return kilograms;
    }

    /**
     * Generate a collision shape for a shaft.
     *
     * @param length length of the shaft (in meters, &gt;0)
     * @param radius radius of the shaft (in meters, &gt;0)
     * @return a new collision shape
     */
    private CollisionShape createCollisionShape(float length,
            float radius) {
        assert radius > 0f : radius;
        assert length > radius : length;

        float scaleFactor = scene.getScaleFactor();
        CollisionShape shape = new CapsuleCollisionShape(radius / scaleFactor,
                length / scaleFactor, PhysicsSpace.AXIS_Z);

        return shape;
    }
}